﻿#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using UnityEngine;

namespace XYRendering.Vegetation
{
    
    public class VGInstancingDataBakePreviewSetting
    {
        public bool showBounds = true;
        public Color boundsColor = Color.yellow;
        public bool showBlock   = true;
        public Color blockColor = Color.gray;
        public bool showBlockIndex = true;
        public Color indexColor = Color.red;

    }


    
    public sealed class VGInstancingDataBakeSetting
    {
        public GameObject placingGO;
        public LayerMask layerMask = -1;
        public int maxDivision = 64;
        public int division = 8;
        public float blockHeightOffset = 0f;
        public bool includeInactive = false;
        public bool autoHeightAdaption = true;
        public bool usingVGRendererWhenBakedFinish = true;
        public bool removePaintedObjsWhenBakeFinish = true;
        public VGInstancingData data;

        public VGInstancingDataBakeSetting Copy()
        {
            var copy = new VGInstancingDataBakeSetting();
            copy.placingGO = this.placingGO;
            copy.layerMask = this.layerMask;
            copy.maxDivision = this.maxDivision;
            copy.division = this.division;
            copy.blockHeightOffset = this.blockHeightOffset;
            copy.includeInactive = this.includeInactive;
            copy.autoHeightAdaption = this.autoHeightAdaption;
            copy.removePaintedObjsWhenBakeFinish = this.removePaintedObjsWhenBakeFinish;
            copy.usingVGRendererWhenBakedFinish = this.usingVGRendererWhenBakedFinish;
            copy.data = this.data;
            
            return copy;
        }
    }

    
    [Flags]
    public enum VGInstancingDataBakeError
    {
        NoError = 0,
        UnSpecifyBakingData = 1 << 0,       //未指定烘焙数据
        UnSpecifyTerrain = 1 << 1,          //未指定地表GameObject
        UnsupportedTerrain = 1 << 2,        //不支持的地表（仅支持美术建模地表或者UUnity Terrain）
        UnvaildDivision = 1 << 3,
    }

    public struct VGInstancingDataBakeResult
    {
       public VGInstancingDataBakeError error;
       public int vgInstanceCount;
       public int bakedVgInstanceCount;
       public int multiSubMeshCount;
       public int outOfBlocksCount;
       public int skipVgInstanceCount;

       public VGInstancingDataBakeResult(VGInstancingDataBakeError e)
       {
           error = VGInstancingDataBakeError.NoError;
           vgInstanceCount = 0;
           bakedVgInstanceCount = 0;
           multiSubMeshCount = 0;
           outOfBlocksCount = 0;
           skipVgInstanceCount = 0;
       }
    }


    public class VGInstancingDataBaker
    {
        public static VGInstancingDataBakeError ChechError(VGInstancingDataBakeSetting setting)
        {
            VGInstancingDataBakeError e = VGInstancingDataBakeError.NoError;
            e |= setting.data== null ? VGInstancingDataBakeError.UnSpecifyBakingData : VGInstancingDataBakeError.NoError;
            e |= setting.placingGO == null ? VGInstancingDataBakeError.UnSpecifyTerrain : VGInstancingDataBakeError.NoError;
            e |= setting.division <= 1 ? VGInstancingDataBakeError.UnvaildDivision : VGInstancingDataBakeError.NoError;

            if (setting.placingGO)
            {
                if (!(setting.placingGO.TryGetComponent<Terrain>(out var _) || setting.placingGO.TryGetComponent<Renderer>(out var _)))
                    e |= VGInstancingDataBakeError.UnsupportedTerrain;
            }
            
            return e;
        }
    
        public static VGInstancingDataBakeResult Bake(VGInstancingDataBakeSetting setting, Action<float, string> progressUI, Action finishUI)
        {
            // Error Check
            VGInstancingDataBakeResult result = new VGInstancingDataBakeResult(VGInstancingDataBakeError.NoError);
            VGInstancingData bakingData = setting.data;
            result.error = ChechError(setting);

            if (result.error != VGInstancingDataBakeError.NoError)
                return result;
            
            //Renderer terrainRenderer = setting.placingGO.GetComponent<Renderer>();
                        
            // Terrain division
            MeshDivisionUtil terrainDivitor = new MeshDivisionUtil();
            terrainDivitor.Divide(setting.placingGO, setting.division, setting.autoHeightAdaption);
            
            
            // Collect instance data
            bakingData.ResetData();
            bakingData.blockHeightOffset = setting.blockHeightOffset;
            
            var vgRenderers = setting.placingGO.GetComponentsInChildren<Renderer>(setting.includeInactive);

            int itr = 0;
            Vector3 offset = new Vector3(0, -setting.blockHeightOffset, 0);
            foreach (var vgRenderer in vgRenderers)
            {
                float progress = (float)itr / vgRenderers.Length;
               
                if (progressUI != null)
                     progressUI(progress, vgRenderers[itr].gameObject.name);
               
                itr++;
                
                if (ReferenceEquals(vgRenderer.gameObject, setting.placingGO))
                    continue;

                if ( (setting.layerMask & (1 << vgRenderer.gameObject.layer)) != 0 
                     && vgRenderer.TryGetComponent<MeshFilter>(out var vgMesh)
                     && vgRenderer.sharedMaterial != null
                     && vgMesh.sharedMesh != null)
                {
                    result.vgInstanceCount++;
                    
                    if (vgMesh.sharedMesh.subMeshCount > 1)
                    {
                        result.multiSubMeshCount++;
                        continue;
                    }
                    
                    
                    Matrix4x4 worldMatrix = vgRenderer.transform.localToWorldMatrix;
                    Vector3 pos = worldMatrix.MultiplyPoint(Vector3.zero) + offset;
                    if (terrainDivitor.PointInDivisionBound(pos, out var blockIdx, out var blockBounds))
                    {
                        var prefab = bakingData.GetPrefab(vgMesh.sharedMesh, vgRenderer.sharedMaterial);
                        if (prefab == null)
                            prefab = bakingData.AddPrefab(vgMesh.sharedMesh, vgRenderer.sharedMaterial, vgRenderer.transform.parent);
                            
                        var block = bakingData.GetBlock(blockIdx);
                        if (block == null)
                            block = bakingData.AddBlock(blockIdx, blockBounds);
                            
                        int prefabIdx = bakingData.prefabs.FindIndex(p => p.Equals(vgMesh.sharedMesh, vgRenderer.sharedMaterial));
                        
                        VGInstanceData ins = new VGInstanceData();
                        ins.name = vgRenderer.gameObject.name;
                        ins.worldMatrix = worldMatrix;
                        ins.prefabIndex = prefabIdx;
                        ins.layer = vgRenderer.transform.gameObject.layer;
                        ins.localPosition = vgRenderer.transform.localPosition;
                        ins.localScale = vgRenderer.transform.localScale;
                        ins.localRotation = vgRenderer.transform.localEulerAngles;
                        
                        prefab.instancesCount++;
                        
                        block.instances.Add(ins);
                        
                        result.bakedVgInstanceCount++;
                    }
                    else
                    {
                       result.outOfBlocksCount++;
                    }

                }
            }
        
            result.skipVgInstanceCount = result.multiSubMeshCount + result.outOfBlocksCount;
            
            if (finishUI != null)
                finishUI();

            return result;
        }
        
        static Bounds LocalToWorldBound(Bounds b, Matrix4x4 transform)
        {
            Vector3[] corners = new Vector3[8];
            corners[0] = b.min;
            corners[1] = new Vector3(b.min.x, b.max.y, b.min.z);
            corners[2] = new Vector3(b.max.x, b.max.y, b.min.z);
            corners[3] = new Vector3(b.max.x, b.min.y, b.min.z); 
        
            corners[4] = b.max;
            corners[5] = new Vector3(b.max.x, b.min.y, b.max.z);
            corners[6] = new Vector3(b.min.x, b.min.y, b.max.z);
            corners[7] = new Vector3(b.min.x, b.max.y, b.max.z);

            for (int i = 0; i < 8; i++)
            {
                corners[i] = transform.MultiplyPoint(corners[i]);
            }
        
            Vector3 min = corners[0];
            Vector3 max = corners[0];

            for (int i = 0; i < 8; i++)
            {
                min.x = Mathf.Min(corners[i].x, min.x);
                min.y = Mathf.Min(corners[i].y, min.y);
                min.z = Mathf.Min(corners[i].z, min.z);
            
                max.x = Mathf.Max(corners[i].x, max.x);
                max.y = Mathf.Max(corners[i].y, max.y);
                max.z = Mathf.Max(corners[i].z, max.z);
            }

            Bounds a = new Bounds();
            a.SetMinMax(min, max);
        
            return a;
        }
    }


    public class MeshDivisionUtil
    {

        private List<Bounds> m_DividedBounds = new List<Bounds>();
        private List<Vector2Int> m_DivideBoundsIndices = new List<Vector2Int>();
        
        public int numBounds => m_DividedBounds.Count;
        
        public KeyValuePair<Vector2, Bounds> this[int idx] => new KeyValuePair<Vector2, Bounds>(m_DivideBoundsIndices[idx], m_DividedBounds[idx]);
        

        public void Divide(GameObject ground, int n, bool autoHeightAdaption)
        {
            m_DividedBounds.Clear();
            m_DivideBoundsIndices.Clear();

            Terrain terrain;
            Renderer renderer;
            bool hasRenderer = ground.TryGetComponent<Renderer>(out renderer);
            bool hasTerrain = ground.TryGetComponent<Terrain>(out terrain);
            
            if (!hasRenderer && !hasTerrain)
                return;

            var bounds = renderer ? renderer.bounds : new Bounds(terrain.terrainData.bounds.center+terrain.transform.position,terrain.terrainData.size) ;
               
            float maxSize = Mathf.Max(bounds.size.x, bounds.size.z);
            float blockSize = maxSize / n;

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    Bounds bound = new Bounds();
                    Vector3 min = new Vector3( bounds.min.x + j * blockSize, bounds.center.y - blockSize / 2, bounds.min.z + i * blockSize);
                    Vector3 max = new Vector3(min.x + blockSize, min.y + blockSize , min.z + blockSize);
                    bound.SetMinMax(min, max);
                    m_DividedBounds.Add(bound);
                    m_DivideBoundsIndices.Add(new Vector2Int(i, j));
                }
            }

            for (int j = m_DividedBounds.Count-1; j >= 0; j--)
            {
                if (!m_DividedBounds[j].Intersects(bounds))
                {
                    m_DividedBounds.RemoveAt(j);
                    m_DivideBoundsIndices.RemoveAt(j);
                }
            }

            if (autoHeightAdaption)
            {
                TerrainCollider tc = null;
                MeshCollider mc = null;
                bool hasCollider = terrain
                    ? ground.TryGetComponent<TerrainCollider>(out tc)
                    : ground.TryGetComponent<MeshCollider>(out mc);
                if (!hasCollider)
                {
                    if (terrain)
                    {
                        tc = ground.AddComponent<TerrainCollider>();
                    }
                    else
                    {
                        mc = ground.AddComponent<MeshCollider>();
                        mc.sharedMesh = renderer.GetComponent<MeshFilter>().sharedMesh;
                    }
                }
                
               
                for (int i = 0; i < m_DividedBounds.Count; i++)
                {
                    Ray ray = new Ray();
                    RaycastHit result = new RaycastHit();
                    
                    ray.origin = m_DividedBounds[i].center;
                    ray.direction = Vector3.down;

                    bool hited = Physics.Raycast(ray, out result, 1000, 1 << ground.layer);
                    if (!hited)
                    {
                        Physics.queriesHitBackfaces = true;
                        ray.direction = Vector3.up;
                        hited = Physics.Raycast(ray, out result, 1000, 1 << ground.layer);
                        Physics.queriesHitBackfaces = false;
                    }

                    if (hited)
                        m_DividedBounds[i] = new Bounds(result.point, m_DividedBounds[i].size);
                }

                if (!hasCollider)
                {
                    UnityEngine.Object.DestroyImmediate(mc);
                    UnityEngine.Object.DestroyImmediate(tc);   
                }
            }
           
        }

        public bool PointInDivisionBound(Vector3 point, out Vector2Int index, out Bounds bounds)
        {
            index = new Vector2Int(-1, -1);
            bounds = new Bounds();
            
            for (int i = 0; i < m_DividedBounds.Count; i++)
            {
                if (m_DividedBounds[i].Contains(point))
                {
                    index = m_DivideBoundsIndices[i];
                    bounds = m_DividedBounds[i];
                    return true;
                }
            }
            
            return false;
        }
        
    }
}


#endif